﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;

namespace DaveSexton.Labs
{
	public static class ConsoleFormat
	{
		public static string Wrap(string message)
		{
			Contract.Ensures(Contract.Result<string>() != null);

			return Wrap(string.Empty, message, Console.WindowWidth);
		}

		public static string Wrap(string message, int maximumWidth)
		{
			Contract.Ensures(Contract.Result<string>() != null);

			return Wrap(string.Empty, message, maximumWidth);
		}

		public static string Wrap(string linePrefix, string message)
		{
			Contract.Requires(linePrefix != null);
			Contract.Ensures(Contract.Result<string>() != null);

			return Wrap(linePrefix, message, Console.WindowWidth);
		}

		public static string Wrap(string linePrefix, string message, int maximumWidth)
		{
			Contract.Requires(linePrefix != null);
			Contract.Ensures(Contract.Result<string>() != null);

			var lineIndentLength = linePrefix.Length + 2;
			var lineLength = maximumWidth - lineIndentLength;

			var words = Words(message, lineLength);

			int charCount = 0;

			Func<string, bool> nextWordExceedsLineLimit = word =>
			{
				return (charCount += word.Length) >= lineLength;
			};

			int currentLine = 0;
			bool skipWhitespace = false;

			Func<bool> shouldSkipWord = () =>
			{
				var skip = skipWhitespace;

				skipWhitespace = false;

				return skip;
			};

			Func<string, bool, int> nextLine = (word, isNewLine) =>
			{
				if (!isNewLine && char.IsWhiteSpace(word[0]))
				{
					charCount = 0;
					skipWhitespace = true;
				}
				else
					charCount = word.Length;

				return ++currentLine;
			};

			var value =
				(from word in words
				 let isNewLine = string.Equals(word, Environment.NewLine, StringComparison.Ordinal)
				 let lineIndex = isNewLine || nextWordExceedsLineLimit(word) ? nextLine(word, isNewLine) : currentLine
				 where !shouldSkipWord()
				 group word by lineIndex into lineWords
				 select lineWords
					 .Aggregate(
						 new StringBuilder(),
						 (builder, word) => builder.Append(word),
						 builder => builder.ToString().Trim()))
					 .Aggregate(
							new StringBuilder(),
							(builder, line) => builder.Append(linePrefix).AppendLine(line),
							builder => builder.ToString());

			Contract.Assume(value != null);

			return value;
		}

		private static IEnumerable<string> Words(string message, int lineLength)
		{
			Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);

			bool previousCharIsWhiteSpace = false;
			int currentWord = 0;

			Func<char, bool> startNextWord = c =>
			{
				return char.IsWhiteSpace(c) != previousCharIsWhiteSpace;
			};

			Func<int> nextWord = () =>
			{
				previousCharIsWhiteSpace = !previousCharIsWhiteSpace;
				return ++currentWord;
			};

			return from c in message ?? string.Empty
						 let wordIndex = startNextWord(c) ? nextWord() : currentWord
						 from expanded in c == '\t' ? new[] { ' ', ' ' } : new[] { c }
						 group expanded by wordIndex into chars
						 from word in BreakWords(chars.ToArray(), lineLength - 1)
						 where word.Length > 0
						 select word;
		}

		private static IEnumerable<string> BreakWords(char[] chars, int maximumLength)
		{
			Contract.Requires(chars != null);
			Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);

			return BreakWordsIterator(chars, maximumLength);
		}

		private static IEnumerable<string> BreakWordsIterator(char[] chars, int maximumLength)
		{
			if (chars.Length <= maximumLength)
			{
				if (char.IsWhiteSpace(chars[0]))
				{
					foreach (var whitespace in BreakNewLines(chars))
					{
						yield return whitespace;
					}
				}
				else
				{
					yield return new string(chars);
				}
			}
			else if (char.IsWhiteSpace(chars[0]))
			{
				yield return " ";
			}
			else
			{
				int partsCount = chars.Length / maximumLength;

				for (int p = 0; p < partsCount; p++)
				{
					yield return new string(chars, p * maximumLength, maximumLength);
				}

				int remainderIndex = partsCount * maximumLength;

				if (chars.Length > remainderIndex)
				{
					yield return new string(chars, remainderIndex, chars.Length - remainderIndex);
				}
			}
		}

		private static IEnumerable<string> BreakNewLines(char[] chars)
		{
			Contract.Requires(chars != null);
			Contract.Ensures(Contract.Result<IEnumerable<string>>() != null);

			return BreakNewLinesIterator(chars);
		}

		private static IEnumerable<string> BreakNewLinesIterator(char[] chars)
		{
			var newLine = Environment.NewLine;
			var newLineIndex = 0;
			var remainderIndex = 0;

			for (int i = 0; i < chars.Length; i++)
			{
				var c = chars[i];

				if (c == newLine[newLineIndex])
				{
					if (newLine.Length == newLineIndex + 1)
					{
						var remainderLength = (i - (newLine.Length - 1)) - remainderIndex;

						if (remainderLength > 0)
						{
							yield return new string(chars, remainderIndex, remainderLength);
						}

						yield return newLine;

						remainderIndex = i + 1;
						newLineIndex = 0;
					}
					else
					{
						newLineIndex++;
					}
				}
				else
				{
					newLineIndex = 0;
				}
			}

			if (remainderIndex < chars.Length)
			{
				yield return new string(chars, remainderIndex, chars.Length - remainderIndex);
			}
		}
	}
}